/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
function processFunc (coreContext, func) {
  let argNames = ((func || '').toString()
    .replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '') // strip comments
    .match(/^(function)?\s*[^\(]*\(\s*([^\)]*)\)/m) || ['', '', ''])[2] // extract parameters
    .split(',') // split parameters
    .map(item => item.replace(/^\s*(_?)(.+?)\1\s*$/, name => name.split('=')[0].trim())) // handle parameters with default value
    .filter(String)
  let funcLen = func.length
  let processedFunc
  switch (funcLen) {
    case 0: {
      if (Object.prototype.toString.call(func) === '[object AsyncFunction]') {
        coreContext.getDefaultService('config').setSupportAsync(true)
      }
      processedFunc = func
      break
    }
    case 1: {
      if (argNames[0] === 'data') {
        processedFunc = function (paramItem) {
          func(paramItem)
        }
      } else {
        coreContext.getDefaultService('config').setSupportAsync(true)
        processedFunc = function () {
          return new Promise(resolve => {
            function done () {
              resolve()
            }

            func(done)
          })
        }
      }
      break
    }
    default: {
      coreContext.getDefaultService('config').setSupportAsync(true)
      processedFunc = function (paramItem) {
        return new Promise(resolve => {
          function done () {
            resolve()
          }

          func(done, paramItem)
        })
      }
      break
    }
  }
  return processedFunc
}

function secureRandomNumber () { // Random numbers between 0 and 1, similar to math.random
  return crypto.randomBytes(8).readUInt32LE() / 0xffffffff
}

class SuiteService {
  constructor (attr) {
    this.id = attr.id
    this.rootSuite = new SuiteService.Suite({})
    this.currentRunningSuite = this.rootSuite
  }

  describe (desc, func) {
    if (this.coreContext.getDefaultService('config').filterSuite(desc)) {
      return
    }
    const suite = new SuiteService.Suite({ description: desc })
    if (typeof this.coreContext.getServices('dataDriver') !== 'undefined') {
      let suiteStress = this.coreContext.getServices('dataDriver').dataDriver.getSuiteStress(desc)
      for (let i = 1; i < suiteStress; i++) {
        this.currentRunningSuite.childSuites.push(suite)
      }
    }
    const currentSuiteCache = this.currentRunningSuite
    this.currentRunningSuite.childSuites.push(suite)
    this.currentRunningSuite = suite
    func.call()
    this.currentRunningSuite = currentSuiteCache
  }

  beforeAll (func) {
    this.currentRunningSuite.beforeAll.push(processFunc(this.coreContext, func))
  }

  beforeEach (func) {
    this.currentRunningSuite.beforeEach.push(processFunc(this.coreContext, func))
  }

  afterAll (func) {
    this.currentRunningSuite.afterAll.push(processFunc(this.coreContext, func))
  }

  afterEach (func) {
    this.currentRunningSuite.afterEach.push(processFunc(this.coreContext, func))
  }

  getCurrentRunningSuite () {
    return this.currentRunningSuite
  }

  setCurrentRunningSuite (suite) {
    this.currentRunningSuite = suite
  }

  getSummary () {
    let total = 0
    let error = 0
    let failure = 0
    let rootSuite = this.coreContext.getDefaultService('suite').rootSuite
    if (rootSuite && rootSuite.childSuites) {
      for (let i = 0; i < rootSuite.childSuites.length; i++) {
        let testsuite = rootSuite.childSuites[i]
        let specs = testsuite['specs']
        for (let j = 0; j < specs.length; j++) {
          total++
          let testcase = specs[j]
          if (testcase.error) {
            error++
          } else if (testcase.result.failExpects.length > 0) {
            failure++
          }
        }
      }
    }
    return { total: total, failure: failure, error: error }
  }

  init (coreContext) {
    this.coreContext = coreContext
  }

  execute () {
    this.coreContext.fireEvents('task', 'taskStart')
    if (this.coreContext.getDefaultService('config').isSupportAsync()) {
      let asyncExecute = async () => {
        await this.rootSuite.asyncRun(this.coreContext)
      }
      asyncExecute().then(() => {
        this.coreContext.fireEvents('task', 'taskDone')
      })
    } else {
      this.rootSuite.run(this.coreContext)
      this.coreContext.fireEvents('task', 'taskDone')
    }
  }

  apis () {
    const _this = this
    return {
      describe: function (desc, func) {
        return _this.describe(desc, func)
      },
      beforeAll: function (func) {
        return _this.beforeAll(func)
      },
      beforeEach: function (func) {
        return _this.beforeEach(func)
      },
      afterAll: function (func) {
        return _this.afterAll(func)
      },
      afterEach: function (func) {
        return _this.afterEach(func)
      }
    }
  }
}

SuiteService.Suite = class {
  constructor (attrs) {
    this.description = attrs.description || ''
    this.childSuites = []
    this.specs = []
    this.beforeAll = []
    this.afterAll = []
    this.beforeEach = []
    this.afterEach = []
    this.duration = 0
  }

  pushSpec (spec) {
    this.specs.push(spec)
  }

  removeSpec (desc) {
    this.specs = this.specs.filter((item, index) => {
      return item.description !== desc
    })
  }

  getSpecsNum () {
    return this.specs.length
  }

  run (coreContext) {
    if (this.description != '') {
      coreContext.fireEvents('suite', 'suiteStart', this)
    }
    const suiteService = coreContext.getDefaultService('suite')
    this.runHookFunc('beforeAll')
    if (this.specs.length > 0) {
      const configService = coreContext.getDefaultService('config')
      suiteService.setCurrentRunningSuite(this)
      if (configService.isRandom()) {
        this.specs.sort(function () {
          return secureRandomNumber() > 0.5 ? -1 : 1
        })
      }
      this.specs.forEach(spec => {
        this.runHookFunc('beforeEach')
        spec.run(coreContext)
        this.runHookFunc('afterEach')
      })
    }

    if (this.childSuites.length > 0) {
      this.childSuites.forEach(childSuite => {
        childSuite.run(coreContext)
        suiteService.setCurrentRunningSuite(childSuite)
      })
    }

    this.runHookFunc('afterAll')
    if (this.description != '') {
      coreContext.fireEvents('suite', 'suiteDone')
    }
  }

  asyncRun (coreContext) {
    return new Promise(async resolve => {
      if (this.description != '') {
        coreContext.fireEvents('suite', 'suiteStart', this)
      }
      const suiteService = coreContext.getDefaultService('suite')
      await this.runAsyncHookFunc('beforeAll')
      if (this.specs.length > 0) {
        const configService = coreContext.getDefaultService('config')
        suiteService.setCurrentRunningSuite(this)
        if (configService.isRandom()) {
          this.specs.sort(function () {
            return secureRandomNumber() > 0.5 ? -1 : 1
          })
        }
        for (let i = 0; i < this.specs.length; i++) {
          await this.runAsyncHookFunc('beforeEach')
          await this.specs[i].asyncRun(coreContext)
          await this.runAsyncHookFunc('afterEach')
        }
      }

      if (this.childSuites.length > 0) {
        for (let i = 0; i < this.childSuites.length; i++) {
          suiteService.setCurrentRunningSuite(this.childSuites[i])
          await this.childSuites[i].asyncRun(coreContext)
        }
      }

      await this.runAsyncHookFunc('afterAll')
      if (this.description != '') {
        coreContext.fireEvents('suite', 'suiteDone')
      }
      resolve()
    })
  }

  runHookFunc (hookName) {
    if (this[hookName] && this[hookName].length > 0) {
      this[hookName].forEach(func => {
        try {
          func()
        } catch (e) {
          console.error(e)
        }
      })
    }
  }

  runAsyncHookFunc (hookName) {
    if (this[hookName] && this[hookName].length > 0) {
      return new Promise(async resolve => {
        for (let i = 0; i < this[hookName].length; i++) {
          try {
            await this[hookName][i]()
          } catch (e) {
            console.error(e)
          }
        }
        resolve()
      })
    }
  }
}

class SpecService {
  constructor (attr) {
    this.id = attr.id
  }

  init (coreContext) {
    this.coreContext = coreContext
  }

  setCurrentRunningSpec (spec) {
    this.currentRunningSpec = spec
  }

  getCurrentRunningSpec () {
    return this.currentRunningSpec
  }

  it (desc, filter, func) {
    const configService = this.coreContext.getDefaultService('config')
    if (configService.filterDesc(desc, filter, this.coreContext)) {
      return
    }
    let processedFunc = processFunc(this.coreContext, func)
    const spec = new SpecService.Spec({ description: desc, fi: filter, fn: processedFunc })
    const suiteService = this.coreContext.getDefaultService('suite')
    if (typeof this.coreContext.getServices('dataDriver') !== 'undefined') {
      let specStress = this.coreContext.getServices('dataDriver').dataDriver.getSpecStress(desc)
      for (let i = 1; i < specStress; i++) {
        suiteService.getCurrentRunningSuite().pushSpec(spec)
      }
    }
    suiteService.getCurrentRunningSuite().pushSpec(spec)
  }

  apis () {
    const _this = this
    return {
      it: function (desc, filter, func) {
        return _this.it(desc, filter, func)
      }
    }
  }
}

SpecService.Spec = class {
  constructor (attrs) {
    this.description = attrs.description || ''
    this.fi = attrs.fi
    this.fn = attrs.fn || function () {
    }
    this.result = {
      failExpects: [],
      passExpects: []
    }
    this.error = undefined
    this.duration = 0
  }

  run (coreContext) {
    const specService = coreContext.getDefaultService('spec')
    specService.setCurrentRunningSpec(this)
    coreContext.fireEvents('spec', 'specStart', this)
    let startTime = new Date().getTime()
    try {
      let dataDriver = coreContext.getServices('dataDriver')
      if (typeof dataDriver === 'undefined') {
        this.fn()
      } else {
        let suiteParams = dataDriver.dataDriver.getSuiteParams()
        let specParams = dataDriver.dataDriver.getSpecParams()
        console.info('[suite params] ' + JSON.stringify(suiteParams))
        console.info('[spec params] ' + JSON.stringify(specParams))
        if (this.fn.length === 0) {
          this.fn()
        } else if (specParams.length === 0) {
          this.fn(suiteParams)
        } else {
          specParams.forEach(paramItem => this.fn(Object.assign({}, paramItem, suiteParams)))
        }
      }
    } catch (e) {
      this.error = e
    }
    let endTime = new Date().getTime()
    this.duration = ((endTime - startTime) / 1000).toFixed(3)
    coreContext.fireEvents('spec', 'specDone', this)
  }

  asyncRun (coreContext) {
    const specService = coreContext.getDefaultService('spec')
    specService.setCurrentRunningSpec(this)
    return new Promise(async resolve => {
      coreContext.fireEvents('spec', 'specStart', this)
      let startTime = new Date().getTime()
      try {
        let dataDriver = coreContext.getServices('dataDriver')
        if (typeof dataDriver === 'undefined') {
          await this.fn()
        } else {
          let suiteParams = dataDriver.dataDriver.getSuiteParams()
          let specParams = dataDriver.dataDriver.getSpecParams()
          console.info('[suite params] ' + JSON.stringify(suiteParams))
          console.info('[spec params] ' + JSON.stringify(specParams))
          if (this.fn.length === 0) {
            await this.fn()
          } else if (specParams.length === 0) {
            await this.fn(suiteParams)
          } else {
            specParams.forEach(async paramItem => await this.fn(Object.assign({}, paramItem, suiteParams)))
          }
        }
      } catch (e) {
        this.error = e
      }
      let endTime = new Date().getTime()
      this.duration = ((endTime - startTime) / 1000).toFixed(3)
      coreContext.fireEvents('spec', 'specDone', this)
      resolve()
    })
  }

  filterCheck (coreContext) {
    const specService = coreContext.getDefaultService('spec')
    specService.setCurrentRunningSpec(this)
    return true
  }

  addExpectationResult (expectResult) {
    if (expectResult.pass) {
      this.result.passExpects.push(expectResult)
    } else {
      this.result.failExpects.push(expectResult)
    }
  }
}

class ExpectService {
  constructor (attr) {
    this.id = attr.id
    this.matchers = {}
  }

  expect (actualValue) {
    return this.wrapMatchers(actualValue)
  }

  init (coreContext) {
    this.coreContext = coreContext
    this.addMatchers(this.basicMatchers())
  }

  addMatchers (matchers) {
    for (const matcherName in matchers) {
      this.matchers[matcherName] = matchers[matcherName]
    }
  }

  basicMatchers () {
    return {
      assertTrue: function (actualValue) {
        return {
          pass: actualValue === true
        }
      },
      assertEqual: function (actualValue, args) {
        return {
          pass: actualValue == args[0],
          expectValue: args[0]
        }
      },
      assertThrow: function (actual, args) {
        const result = {
          pass: false
        }
        if (typeof actual !== 'function') {
          result.message = 'toThrow\'s Actual should be a Function'
        } else {
          let hasThrow = false
          let throwError
          try {
            actual()
          } catch (e) {
            hasThrow = true
            throwError = e
          }
          if (!hasThrow) {
            result.message = 'function did not throw an exception'
          } else {
            if (throwError && throwError.message === args[0]) {
              result.pass = true
            } else {
              result.message = `expect to throw ${args[0]} , actual throw ${throwError.message}`
            }
          }
        }
        return result
      }
    }
  }

  wrapMatchers (actualValue) {
    const _this = this
    const wrappedMatchers = {}
    const specService = _this.coreContext.getDefaultService('spec')
    const currentRunningSpec = specService.getCurrentRunningSpec()
    for (const matcherName in this.matchers) {
      wrappedMatchers[matcherName] = function () {
        const result = _this.matchers[matcherName](actualValue, arguments)
        result.actualValue = actualValue
        result.checkFunc = matcherName
        currentRunningSpec.addExpectationResult(result)
      }
    }
    return wrappedMatchers
  }

  apis () {
    const _this = this
    return {
      expect: function (actualValue) {
        return _this.expect(actualValue)
      }
    }
  }
}

class ReportService {
  constructor (attr) {
    this.id = attr.id
  }

  init (coreContext) {
    this.coreContext = coreContext
    this.specService = this.coreContext.getDefaultService('spec')
    this.suiteService = this.coreContext.getDefaultService('suite')
    this.duration = 0
  }

  taskStart () {
    this.taskStartTime = new Date().getTime()
    this.sleep(200)
    console.info('[start] start run suites')
  }

  suiteStart () {
    this.sleep(200)
    console.info('[suite start]' + this.suiteService.getCurrentRunningSuite().description)
  }

  specStart () {
    this.sleep(200)
    console.info('start running case \'' + this.specService.currentRunningSpec.description + '\'')
    this.index = this.index + 1
  }

  specDone () {
    this.sleep(200)

    let msg = ''
    let spec = this.specService.currentRunningSpec
    if (spec.error) {
      this.formatPrint('fail', spec.description)
      this.formatPrint('failDetail', spec.error)
    } else if (spec.result) {
      if (spec.result.failExpects.length > 0) {
        this.formatPrint('fail', spec.description)
        spec.result.failExpects.forEach(failExpect => {
          msg = failExpect.message || ('expect ' + failExpect.actualValue + ' ' + failExpect.checkFunc + ' ' + (failExpect.expectValue || ''))
          this.formatPrint('failDetail', msg)
        })
      } else {
        this.formatPrint('pass', spec.description + ' ; consuming ' + spec.duration + 'S')
      }
    }
    this.formatPrint(this.specService.currentRunningSpec.error, msg)
  }

  suiteDone () {
    this.sleep(200)
    console.info('[suite end]')
  }

  taskDone () {
    this.sleep(200)
    this.taskDoneTime = new Date().getTime()
    this.duration = ((this.taskDoneTime - this.taskStartTime) / 1000).toFixed(2)
    let summary = this.suiteService.getSummary()
    console.info('total cases:' + summary.total + ';failure ' + summary.failure + ',' + 'error ' + summary.error + ',pass ' + (summary.total - summary.error - summary.failure) + '; consuming ' + this.duration + 'S')
    console.info('[end] run suites end')
  }

  formatPrint (type, msg) {
    switch (type) {
      case 'pass':
        console.info('[pass]' + msg)
        break
      case 'fail':
        console.info('[fail] ' + msg)
        break
      case 'failDetail':
        console.info('[failDetail]' + msg)
        break
      default:
        break
    }
  }

  sleep (numberMillis) {
    var now = new Date()
    var exitTime = now.getTime() + numberMillis
    while (true) {
      now = new Date()
      if (now.getTime() > exitTime) {
        return
      }
    }
  }
}

class ConfigService {
  constructor (attr) {
    this.id = attr.id
    this.supportAsync = false
    this.random = false
  }

  init () {

  }

  setConfig (params) {
    try {
      this.filter = params.filter
      this.flag = params.flag || { flag: false }
      this.suite = params.suite
      this.itName = params.itName
    } catch (err) {
      this.filter = 0
      this.flag = false
      this.suite = null
      this.itName = null
    }
  }

  filterSuite (description) {
    if (this.suite != undefined && this.suite != '' && this.suite != null) {
      let suiteArray = this.suite.split(',')
      if (suiteArray.indexOf(description) == -1) {
        console.info('run suite:' + suiteArray + ',filter ' + description)
        return true
      }
      return false
    } else {
      return false
    }
  }

  filterDesc (desc, fi, coreContext) {
    if (this.itName != undefined && this.itName != '' && this.itName != null) {
      let itNameArray = this.itName.split(',')
      if (itNameArray.indexOf(desc) == -1) {
        console.info('run it :' + itNameArray + ',filter ' + desc)
        return true
      }
    }
    if (this.filter == undefined || this.filter === 0 || fi === 0) {
      return false
    }
    let testTypeFi = fi & 0B1111111111111111
    let testType = this.filter & 0B1111111111111111
    if (testType != testTypeFi && testTypeFi != 0 && testType != 0) {
      return true
    }
    let sizeFi = fi & 0B11111111 << 16
    let size = this.filter & 0B11111111 << 16
    if (size != 0 && sizeFi != 0 && size != sizeFi) {
      return true
    }
    let levelFi = fi & 0B1111111 << 24
    let level = this.filter & 0B1111111 << 24
    if (level != 0 && levelFi != 0 && level != levelFi) {
      return true
    }
  }

  isRandom () {
    return this.random || false
  }

  setSupportAsync (value) {
    this.supportAsync = value
  }

  isSupportAsync () {
    return this.supportAsync
  }

  execute () {

  }
}

export {
  SuiteService,
  SpecService,
  ExpectService,
  ReportService,
  ConfigService
}
